package org.net5ijy.security.config.security;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;

import org.net5ijy.security.config.AppConfig;
import org.net5ijy.security.util.RequestUtil;
import org.net5ijy.security.util.ResponseMessage;
import org.net5ijy.security.util.ResponseUtil;
import org.net5ijy.security.validate.ValidateCodeValidator;
import org.net5ijy.security.web.filter.ValidateCodeFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.filter.DelegatingFilterProxy;

import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * SecurityConfig
 *
 * @author XGF
 * @date 2020/11/30 22:32
 */
@Configuration
@PropertySource(value = {"classpath:application.properties"})
public class SecurityConfig extends WebSecurityConfigurerAdapter implements
    SecurityConstant {

  private static Logger log = LoggerFactory.getLogger(SecurityConfig.class);

  /**
   * 注入spring容器
   */
  @Autowired
  private WebApplicationContext wac;

  /**
   * 注入properties配置环境
   */
  @Autowired
  private Environment environment;

  @Autowired
  @Qualifier("customUserDetailsService")
  private UserDetailsService userDetailsService;

  @Autowired
  @Qualifier("mobileUserDetailsService")
  private UserDetailsService mobileUserDetailsService;

  @Autowired
  private DataSource dataSource;

  @Autowired
  private PersistentTokenRepository tokenRepository;

  @Autowired
  private AuthenticationFailureHandler authenticationFailureHandler;

  @Autowired
  private MobileSecurityConfig mobileSecurityConfig;

  @Autowired
  private Map<String, ValidateCodeValidator> validateCodeValidators = new HashMap<>();

  @Autowired
  private RedisConnectionFactory redisConnectionFactory;

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(authenticationProvider());
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {

    String[] permitAll = {STATIC_URL, LOGIN_PROCESSING_URL, V_CODE_URL,
        ACCESS_DENIED_FORWARD, LOGIN_ERROR_URL, LOGOUT_PAGE};

    // 获取记住我的token存活时长
    int remembermeTokenValidSeconds = getRemembermeTokenValidSeconds();

    // 权限配置
    http.authorizeRequests()
        // 以下资源都可以访问
        .antMatchers(permitAll)
        .permitAll()
        // 查看用户需要ADMIN或DBA
        .antMatchers("/", "/user/users")
        .hasAnyRole("ADMIN", "DBA")
        // 其余资源需要ADMIN
        .antMatchers("/**")
        .hasRole("ADMIN")
        //
        .and()
        // 登录表单
        .formLogin()
        .loginPage(LOGIN_PAGE)
        .loginProcessingUrl(LOGIN_PROCESSING_URL)
        // 登录失败的处理
        .failureHandler(authenticationFailureHandler)
        // 用户名和密码
        .usernameParameter(FORM_USERNAME_NAME)
        .passwordParameter(FORM_PASSWORD_NAME)
        //
        .and()
        .apply(mobileSecurityConfig)
        .and()
        // 记住我
        .rememberMe()
        .rememberMeParameter(FORM_REMEMBER_ME_NAME)
        // 记住我
        .tokenRepository(tokenRepository)
        .tokenValiditySeconds(remembermeTokenValidSeconds)
        .userDetailsService(userDetailsService)
        //
        .and()
        // 权限不足
        .exceptionHandling()
        .accessDeniedHandler(new CustomAccessDeniedHandler());

    // .csrf().disable();

    // 添加验证码验证过滤器
    ValidateCodeFilter f = new ValidateCodeFilter(
        validateCodeValidators, authenticationFailureHandler);
    f.setUrls(getValidateUrls());
    http.addFilterBefore(f, UsernamePasswordAuthenticationFilter.class);

    // 判断是否开启redis会话管理
    EnableRedisHttpSession enableRedisHttpSession =
        AppConfig.class.getAnnotation(EnableRedisHttpSession.class);
    if (enableRedisHttpSession != null) {
      log.info("开启redis会话管理");
      http.addFilterBefore(
          new DelegatingFilterProxy("springSessionRepositoryFilter", wac),
          WebAsyncManagerIntegrationFilter.class);
    }
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

  @Bean
  public DaoAuthenticationProvider authenticationProvider() {
    DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
    authenticationProvider.setUserDetailsService(userDetailsService);
    authenticationProvider.setPasswordEncoder(passwordEncoder());
    return authenticationProvider;
  }

  @Bean
  public AuthenticationTrustResolver getAuthenticationTrustResolver() {
    return new AuthenticationTrustResolverImpl();
  }

  private int getRemembermeTokenValidSeconds() {
    String value = environment.getProperty("net5ijy.security.rememberme.token.valid.seconds");
    try {
      return Integer.parseInt(value);
    } catch (NumberFormatException e) {
      log.error(e.getMessage(), e);
    }
    return REMEMBERME_TOKEN_VALID_SECONDS;
  }

  private Set<String> getValidateUrls() {
    Set<String> urls = new HashSet<>();
    String urlStr = environment.getProperty("net5ijy.security.login.validateUrl");
    String[] urlArr = urlStr.split(",");
    for (String s : urlArr) {
      urls.add(s.trim());
    }
    return urls;
  }

  private class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request,
        HttpServletResponse response,
        AccessDeniedException accessDeniedException)
        throws IOException, ServletException {

      Authentication auth = SecurityContextHolder.getContext().getAuthentication();

      if (auth == null || ANONYMOUS_USER.equalsIgnoreCase(auth.getName())) {
        log.warn("权限不足 [登录超时]");
        response.sendRedirect(request.getContextPath() + LOGIN_PAGE);
        return;
      }
      // 如果是AJAX请求，返回一个权限不足的JSON回去
      if (RequestUtil.isAjax(request)) {
        // content-type
        response.setContentType(ResponseUtil.APPLICATION_JSON_UTF8);
        // 获取请求URI
        String uri = request.getRequestURI();
        // 设置响应码403
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        // 创建消息对象
        ResponseMessage msg = ResponseMessage.error();
        msg.addAttribute(ResponseMessage.EXCEPTION_ATTR_KEY,
            accessDeniedException.getMessage() + ": " + uri);

        String json = toJson(msg);
        log.warn("权限不足 {}", json);

        // 响应
        response.getWriter().print(json);
      } else {
        log.warn("权限不足，转发至 {}", ACCESS_DENIED_FORWARD);
        request.getRequestDispatcher(ACCESS_DENIED_FORWARD).forward(request, response);
      }
    }
  }

  public static String toJson(Object object) throws IOException {
    ObjectMapper mapper = new ObjectMapper();
    return mapper.writeValueAsString(object);
  }
}
